/*
 * FIRST IDEA FROM:
 * Copyright 2004-2010, Thorbjrn Lindeijer <thorbjorn@lindeijer.nl>
 * Copyright 2004-2006, Adam Turk <aturk@biggeruniverse.com>
 * 
 * FURTHER DEVELOPING:
 * Gianmarco Laggia and Alberto D'Este
 *
 * This file is BASED on libtiled-java.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *    1. Redistributions of source code must retain the above copyright notice,
 *       this list of conditions and the following disclaimer.
 *
 *    2. Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
 * EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
 * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package mh.map;

import static mh.entity.EnumState.BLASTED;
import static mh.entity.EnumState.BROKEN;
import static mh.entity.EnumState.BURNED;
import static mh.entity.EnumState.BURNING;
import static mh.entity.EnumState.CLOSED;
import static mh.entity.EnumState.DAMAGED;
import static mh.entity.EnumState.DUSTY;
import static mh.entity.EnumState.FLOATING;
import static mh.entity.EnumState.FROZEN;
import static mh.entity.EnumState.HIDDEN;
import static mh.entity.EnumState.INTACT;
import static mh.entity.EnumState.LOCKED;
import static mh.entity.EnumState.OPEN;
import static mh.entity.EnumState.WET;
import static mh.entity.EnumState.WRITTEN;

import java.awt.Color;
import java.awt.Image;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.ParseException;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.TreeMap;
import java.util.zip.GZIPInputStream;
import java.util.zip.InflaterInputStream;

import javax.imageio.ImageIO;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import mh.PlayerEntity;
import mh.entity.object.EntityObject;
import mh.interfaces.Constants;
import mh.map.objectset.ObjectSet;
import mh.util.Base64;

import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import tiled.core.AnimatedTile;
import tiled.util.ImageHelper;

/**
 * The standard map reader for TMX files. Supports reading .tmx, .tmx.gz and *.tsx files.
 */
public class TMXMapReader implements Constants {
	private Map							map;
	private String						xmlPath;
	private String						error;
	private final EntityResolver		entityResolver	= new MapEntityResolver();
	private TreeMap<Integer, TileSet>	tilesetPerFirstGid;
	private PlayerEntity				player;

	//	public TMXMapReader() {
	//	}

	/**
	 * Returns any error, if present, null otherwise.
	 * 
	 * @return The error or <code>null</code>
	 */
	public String getError() {
		return this.error;
	}

	private static String makeUrl(String filename) throws MalformedURLException {
		final String url;
		if ((filename.indexOf("://") > 0) || filename.startsWith("file:")) {
			url = filename;
		} else {
			url = new File(filename).toURI().toString();
		}
		return url;
	}

	private static int reflectFindMethodByName(Class<?> c, String methodName) {
		Method[] methods = c.getMethods();
		for (int i = 0; i < methods.length; i++) {
			if (methods[i].getName().equalsIgnoreCase(methodName)) {
				return i;
			}
		}
		return -1;
	}

	private void reflectInvokeMethod(Object invokeVictim, Method method, String[] args) throws Exception {
		Class<?>[] parameterTypes = method.getParameterTypes();
		Object[] conformingArguments = new Object[parameterTypes.length];

		if (args.length < parameterTypes.length) {
			throw new Exception("Insufficient arguments were supplied");
		}

		for (int i = 0; i < parameterTypes.length; i++) {
			if ("int".equalsIgnoreCase(parameterTypes[i].getName())) {
				conformingArguments[i] = new Integer(args[i]);
			} else if ("float".equalsIgnoreCase(parameterTypes[i].getName())) {
				conformingArguments[i] = new Float(args[i]);
			} else if (parameterTypes[i].getName().endsWith("String")) {
				conformingArguments[i] = args[i];
			} else if ("boolean".equalsIgnoreCase(parameterTypes[i].getName())) {
				conformingArguments[i] = Boolean.valueOf(args[i]);
			} else {
				// Unsupported argument type, defaulting to String
				conformingArguments[i] = args[i];
			}
		}

		method.invoke(invokeVictim, conformingArguments);
	}

	private void setOrientation(String o) {
		//		if ("isometric".equalsIgnoreCase(o)) {
		//			map.setOrientation(Map.ORIENTATION_ISOMETRIC);
		//		} else 
		if ("orthogonal".equalsIgnoreCase(o)) {
			this.map.setOrientation(Map.ORIENTATION_ORTHOGONAL);
			//		} else if ("hexagonal".equalsIgnoreCase(o)) {
			//			map.setOrientation(Map.ORIENTATION_HEXAGONAL);
			//		} else if ("shifted".equalsIgnoreCase(o)) {
			//			map.setOrientation(Map.ORIENTATION_SHIFTED);
		} else {
			System.out.println("Unknown orientation '" + o + "'");
		}
	}

	private static String getAttributeValue(Node node, String attribname) {
		final NamedNodeMap attributes = node.getAttributes();
		String value = null;
		if (attributes != null) {
			Node attribute = attributes.getNamedItem(attribname);
			if (attribute != null) {
				value = attribute.getNodeValue();
			}
		}
		return value;
	}

	private static int getAttribute(Node node, String attribname, int def) {
		final String attr = getAttributeValue(node, attribname);
		if (attr != null) {
			return Integer.parseInt(attr);
		} else {
			return def;
		}
	}

	private Object unmarshalClass(Class<?> reflector, Node node) throws InstantiationException, IllegalAccessException, InvocationTargetException {
		Constructor<?> cons = null;
		try {
			cons = reflector.getConstructor((Class[]) null);
		} catch (SecurityException e1) {
			e1.printStackTrace();
		} catch (NoSuchMethodException e1) {
			e1.printStackTrace();
			return null;
		}
		Object o = cons.newInstance((Object[]) null);
		Node n;

		Method[] methods = reflector.getMethods();
		NamedNodeMap nnm = node.getAttributes();

		if (nnm != null) {
			for (int i = 0; i < nnm.getLength(); i++) {
				n = nnm.item(i);

				try {
					int j = reflectFindMethodByName(reflector, "set" + n.getNodeName());
					if (j >= 0) {
						this.reflectInvokeMethod(o, methods[j], new String[] { n.getNodeValue() });
					} else {
						System.out.println("Unsupported attribute '" + n.getNodeName() + "' on <" + node.getNodeName() + "> tag");
					}
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		}
		return o;
	}

	private Image unmarshalImage(Node t, String baseDir) throws IOException {
		Image img = null;

		String source = getAttributeValue(t, "source");

		if (source != null) {
			if (checkRoot(source)) {
				source = makeUrl(source);
			} else {
				source = makeUrl(baseDir + source);
			}
			img = ImageIO.read(new URL(source));
		} else {
			NodeList nl = t.getChildNodes();

			for (int i = 0; i < nl.getLength(); i++) {
				Node node = nl.item(i);
				if ("data".equals(node.getNodeName())) {
					Node cdata = node.getFirstChild();
					if (cdata != null) {
						String sdata = cdata.getNodeValue();
						char[] charArray = sdata.trim().toCharArray();
						byte[] imageData = Base64.decode(charArray);
						img = ImageHelper.bytesToImage(imageData);

						// Deriving a scaled instance, even if it has the same
						// size, somehow makes drawing of the tiles a lot
						// faster on various systems (seen on Linux, Windows
						// and MacOS X).
						img = img.getScaledInstance(img.getWidth(null), img.getHeight(null), Image.SCALE_FAST);
					}
					break;
				}
			}
		}

		return img;
	}

	private TileSet unmarshalTilesetFile(InputStream in, String filename) throws ParserConfigurationException, IOException {
		TileSet set = null;
		Node tsNode;

		DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
		try {
			DocumentBuilder builder = factory.newDocumentBuilder();
			//builder.setErrorHandler(new XMLErrorHandler());
			Document tsDoc = builder.parse(in, ".");

			String xmlPathSave = this.xmlPath;
			if (filename.indexOf(File.separatorChar) >= 0) {
				this.xmlPath = filename.substring(0, filename.lastIndexOf(File.separatorChar) + 1);
			}

			NodeList tsNodeList = tsDoc.getElementsByTagName("tileset");

			// There can be only one tileset in a .tsx file.
			tsNode = tsNodeList.item(0);
			if (tsNode != null) {
				set = this.unmarshalTileset(tsNode);
				if (set.getSource() != null) {
					System.out.println("Recursive external tilesets are not supported.");
				}
				set.setSource(filename);
			}

			this.xmlPath = xmlPathSave;
		} catch (SAXException e) {
			this.error = "Failed while loading " + filename + ": " + e.getLocalizedMessage();
		}

		return set;
	}

	private TileSet unmarshalTileset(Node t) throws MalformedURLException, IOException, ParserConfigurationException {
		String source = getAttributeValue(t, "source");
		String basedir = getAttributeValue(t, "basedir"); //TODO
		int firstGid = getAttribute(t, "firstgid", 1);

		String tilesetBaseDir = this.xmlPath;

		if (basedir != null) {
			tilesetBaseDir = basedir; //makeUrl(basedir);
		}

		if (source != null) {
			String filename = tilesetBaseDir + source;
			//if (checkRoot(source)) {
			//    filename = makeUrl(source);
			//}

			TileSet ext = null;

			try {
				InputStream in = new URL(makeUrl(filename)).openStream();
				ext = this.unmarshalTilesetFile(in, filename);
				this.setFirstGidForTileset(ext, firstGid);
			} catch (FileNotFoundException fnf) {
				this.error = "Could not find external tileset file " + filename;
			}

			if (ext == null) {
				this.error = "Tileset " + source + " was not loaded correctly!";
			}

			return ext;
		} else {
			final int tileWidth = getAttribute(t, "tilewidth", this.map != null ? this.map.getTileWidth() : 0);
			final int tileHeight = getAttribute(t, "tileheight", this.map != null ? this.map.getTileHeight() : 0);
			final int tileSpacing = getAttribute(t, "spacing", 0);
			final int tileMargin = getAttribute(t, "margin", 0);

			final String name = getAttributeValue(t, "name");

			TileSet set = new TileSet();

			boolean hasTilesetImage = false;
			NodeList children = t.getChildNodes();

			for (int i = 0; i < children.getLength(); i++) {
				Node child = children.item(i);

				if (child.getNodeName().equalsIgnoreCase("tileoffset")) {
					//TODO
				} else if (child.getNodeName().equalsIgnoreCase("image")) {
					if (hasTilesetImage) {
						System.out.println("Ignoring illegal image element after tileset image.");
						continue;
					}

					String imgSource = getAttributeValue(child, "source");
					String transStr = getAttributeValue(child, "trans");

					if (imgSource != null) {
						// Not a shared image, but an entire set in one image file. There should be only one image element in this case.
						hasTilesetImage = true;

						// ########################## CACHING ###########################
						CachedResources res = CachedResources.singleton();
						Object o = res.get(imgSource);
						if ((o != null) && (o instanceof TileSet)) {
							set = (TileSet) o;
							this.setFirstGidForTileset(set, firstGid);
							return set;
						}
						//else
						set = new TileSet();
						set.setName(name);
						set.setImgSource(imgSource);
						set.setBaseDir(basedir);
						this.setFirstGidForTileset(set, firstGid);
						res.store(set);

						// ######################### END CACHING #########################

						// FIXME: importTileBitmap does not fully support URLs
						String sourcePath = imgSource;
						if (!new File(imgSource).isAbsolute()) {
							sourcePath = tilesetBaseDir + imgSource;
						}

						if (transStr != null) {
							if (transStr.startsWith("#")) {
								transStr = transStr.substring(1);
							} else if (transStr.startsWith("0x")) {
								transStr = transStr.substring(2);
							}

							int colorInt = Integer.parseInt(transStr, 16);
							Color color = new Color(colorInt);
							set.setTransparentColor(color);
						}
						set.importTileTexture(sourcePath, tileWidth, tileHeight, tileSpacing, tileMargin);// new BasicTileCutter(tileWidth, tileHeight, tileSpacing, tileMargin));
					}
				} else if (child.getNodeName().equalsIgnoreCase("terraintypes")) {
					//TODO
				} else if (child.getNodeName().equalsIgnoreCase("tile")) {

					// ====================
					// This should not be used, each tileset should have an unique image cutted in smaller tiles.
					// ====================

					Tile tile = this.unmarshalTile(set, child, tilesetBaseDir);
					if (!hasTilesetImage || (tile.getId() > set.getMaxTileId())) {
						set.addTile(tile);
					} else {
						Tile myTile = set.getTile(tile.getId());
						myTile.setProperties(tile.getProperties());
						//TODO: there is the possibility here of overlaying images,
						//      which some people may want
					}
				}
			}

			return set;
		}
	}

	private ObjectSet unmarshalObjectFile(InputStream in, String filename) throws ParserConfigurationException, IOException {
		ObjectSet set = null;
		Node osNode;

		DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
		try {
			DocumentBuilder builder = factory.newDocumentBuilder();
			Document osDoc = builder.parse(in, ".");

			String xmlPathSave = this.xmlPath;
			if (filename.indexOf(File.separatorChar) >= 0) {
				this.xmlPath = filename.substring(0, filename.lastIndexOf(File.separatorChar) + 1);
			}

			NodeList osNodeList = osDoc.getElementsByTagName("objectset");

			// There can be only one tileset in a .osx file.
			osNode = osNodeList.item(0);
			if (osNode != null) {
				set = this.unmarshalObjectset(osNode);
				if (set.getSource() != null) {
					System.out.println("Recursive external tilesets are not supported.");
				}
				set.setSource(filename);
			}

			this.xmlPath = xmlPathSave;
		} catch (SAXException e) {
			this.error = "Failed while loading objectset: " + filename + ": " + e.getLocalizedMessage();
		}

		return set;
	}

	private ObjectSet unmarshalObjectset(Node n) throws MalformedURLException, IOException, ParserConfigurationException {
		String source = getAttributeValue(n, "source");
		//		String basedir = getAttributeValue(o, "basedir"); //TODO

		String tilesetBaseDir = this.xmlPath;

		//		if (basedir != null) {
		//			tilesetBaseDir = basedir; //makeUrl(basedir);
		//		}

		if (source != null) {
			//TODO CHECK FROM HERE
			//			String filename = tilesetBaseDir + source;
			//			//if (checkRoot(source)) {
			//			//    filename = makeUrl(source);
			//			//}
			//
			//			ObjectSet ext = null;
			//
			//			try {
			//				InputStream in = new URL(makeUrl(filename)).openStream();
			//				ext = this.unmarshalObjectFile(in, filename);
			//				this.setFirstGidForObjectset(ext, firstGid);
			//			} catch (FileNotFoundException fnf) {
			//				this.error = "Could not find external objectset file " + filename;
			//			}
			//
			//			if (ext == null) {
			//				this.error = "Objectset " + source + " was not loaded correctly!";
			//			}
			//
			//			return ext;
			//			TO HERE
			return null;
		} else {
			ObjectSet set = null;

			final String name = getAttributeValue(n, "name");
			NodeList children = n.getChildNodes();
			boolean hasTilesetImage = false;

			for (int i = 0; i < children.getLength(); i++) {
				Node child = children.item(i);

				if (child.getNodeName().equalsIgnoreCase("image")) {
					if (hasTilesetImage) {
						System.out.println("Ignoring illegal image (2 or more) element after objectset image.");
						continue;
					}

					String imgSource = getAttributeValue(child, "source");
					String transStr = getAttributeValue(child, "trans");

					if (imgSource != null) {
						// ########################## CACHING ###########################
						CachedResources res = CachedResources.singleton();
						Object o = res.get(imgSource);
						if ((o != null) && (o instanceof ObjectSet)) {
							set = (ObjectSet) o;
							//							this.setFirstGidForObjectset(set, firstGid);
							return set;
						}
						//else
						set = new ObjectSet();
						set.setName(name);
						set.setImgSource(imgSource);
						//						set.setBaseDir(basedir);
						//						this.setFirstGidForObjectset(set, firstGid);
						res.store(set);

						// ######################### END CACHING #########################

						// There should be only one image element for each objectset.
						hasTilesetImage = true;

						// FIXME: importTileBitmap does not fully support URLs
						String sourcePath = imgSource;
						if (!new File(imgSource).isAbsolute()) {
							sourcePath = tilesetBaseDir + imgSource;
						}

						if (transStr != null) {
							if (transStr.startsWith("#")) {
								transStr = transStr.substring(1);
							} else if (transStr.startsWith("0x")) {
								transStr = transStr.substring(2);
							}

							int colorInt = Integer.parseInt(transStr, 16);
							Color color = new Color(colorInt);
							set.setTransparentColor(color);
						}
						set.importObjectTexture(sourcePath);
					}
				}
			}
			return set;
		}
	}

	//	@SuppressWarnings("unused")
	//	private MapObject readMapObject(Node t) throws Exception {
	//		final String name = getAttributeValue(t, "name");
	//		final String type = getAttributeValue(t, "type");
	//		final int x = getAttribute(t, "x", 0);
	//		final int y = getAttribute(t, "y", 0);
	//		final int width = getAttribute(t, "width", 0);
	//		final int height = getAttribute(t, "height", 0);
	//
	//		MapObject obj = new MapObject(x, y, width, height);
	//		if (name != null) {
	//			obj.setName(name);
	//		}
	//		if (type != null) {
	//			obj.setType(type);
	//		}
	//
	//		NodeList children = t.getChildNodes();
	//		for (int i = 0; i < children.getLength(); i++) {
	//			Node child = children.item(i);
	//			if ("image".equalsIgnoreCase(child.getNodeName())) {
	//				String source = getAttributeValue(child, "source");
	//				if (source != null) {
	//					if (!new File(source).isAbsolute()) {
	//						source = this.xmlPath + source;
	//					}
	//					obj.setImageSource(source);
	//				}
	//				break;
	//			}
	//		}
	//
	//		Properties props = new Properties();
	//		readProperties(children, props);
	//
	//		obj.setProperties(props);
	//		return obj;
	//	}

	/**
	 * Reads properties from amongst the given children. When a "properties" element is encountered, it recursively
	 * calls itself with the children of this node. This function ensures backward compatibility with tmx version 0.99a.
	 * 
	 * Support for reading property values stored as character data was added in Tiled 0.7.0 (tmx version 0.99c).
	 * 
	 * @param children
	 *            the children amongst which to find properties
	 * @param props
	 *            the properties object to set the properties of
	 */
	private static void readProperties(NodeList children, Properties props) {
		for (int i = 0; i < children.getLength(); i++) {
			Node child = children.item(i);
			if ("property".equalsIgnoreCase(child.getNodeName())) {
				final String key = getAttributeValue(child, "name");
				String value = getAttributeValue(child, "value");
				if (value == null) {
					Node grandChild = child.getFirstChild();
					if (grandChild != null) {
						value = grandChild.getNodeValue();
						if (value != null) {
							value = value.trim();
						}
					}
				}
				if (value != null) {
					props.setProperty(key, value);
				}
			} else if ("properties".equals(child.getNodeName())) {
				readProperties(child.getChildNodes(), props);
			}
		}
	}

	/** @deprecated */
	@Deprecated
	private Tile unmarshalTile(TileSet set, Node t, String baseDir) throws IOException {
		Tile tile = null;
		NodeList children = t.getChildNodes();
		boolean isAnimated = false;

		for (int i = 0; i < children.getLength(); i++) {
			Node child = children.item(i);
			if ("animation".equalsIgnoreCase(child.getNodeName())) {
				isAnimated = true;
				break;
			}
		}

		try {
			if (isAnimated) {
				tile = (Tile) this.unmarshalClass(AnimatedTile.class, t);
			} else {
				tile = (Tile) this.unmarshalClass(Tile.class, t);
			}
		} catch (Exception e) {
			this.error = "Failed creating tile: " + e.getLocalizedMessage();
			return tile;
		}

		tile.setTileSet(set);

		readProperties(children, tile.getProperties());

		for (int i = 0; i < children.getLength(); i++) {
			Node child = children.item(i);
			if ("image".equalsIgnoreCase(child.getNodeName())) {
				int id = getAttribute(child, "id", -1);
				Image img = this.unmarshalImage(child, baseDir);
				//				tile.setImage(img); TODO
			} else if ("animation".equalsIgnoreCase(child.getNodeName())) {
				// TODO: fill this in once TMXMapWriter is complete
			}
		}

		return tile;
	}

	private Layer readObjectLayer(Node t) throws IOException {

		final int layerWidth = getAttribute(t, "width", this.map.getWidth());
		final int layerHeight = getAttribute(t, "height", this.map.getHeight());

		ObjectsLayer ol = new ObjectsLayer(0, 0, layerWidth, layerHeight); // TODO START POINT

		final int visible = getAttribute(t, "visible", 1);
		ol.setName(getAttributeValue(t, "name"));

		readProperties(t.getChildNodes(), ol.getProperties());

		for (Node child = t.getFirstChild(); child != null; child = child.getNextSibling()) {
			String nodeName = child.getNodeName();
			if ("data".equalsIgnoreCase(nodeName)) {
				String encoding = getAttributeValue(child, "encoding");

				if ((encoding != null) && "base64".equalsIgnoreCase(encoding)) {
					Node cdata = child.getFirstChild();
					if (cdata != null) {
						char[] enc = cdata.getNodeValue().trim().toCharArray();
						byte[] dec = Base64.decode(enc);
						ByteArrayInputStream bais = new ByteArrayInputStream(dec);
						InputStream is;

						String comp = getAttributeValue(child, "compression");

						if ("gzip".equalsIgnoreCase(comp)) {
							final int len = layerWidth * layerHeight * 4;
							is = new GZIPInputStream(bais, len);
						} else if ("zlib".equalsIgnoreCase(comp)) {
							is = new InflaterInputStream(bais);
						} else if ((comp != null) && !comp.isEmpty()) {
							throw new IOException("Unrecognized compression method \"" + comp + "\" for ObjectsLayer");
						} else {
							is = bais;
						}

						long actual = System.currentTimeMillis();

						int versionCode = 0;
						versionCode |= is.read();
						versionCode |= is.read() << 8;
						versionCode |= is.read() << 16;
						versionCode |= is.read() << 24;
						if (versionCode == 232323) {
							for (int x = 0; x < this.map.getWidth(); x++) {
								for (int y = 0; y < this.map.getHeight(); y++) {
									int objectId = 0;
									objectId |= is.read();
									objectId |= is.read() << 8;
									ObjectSet os = CachedResources.singleton().getObjectSet(objectId);
									System.out.println("ID: " + objectId + " OS:" + os);

									if (os != null) {
										EntityObject e = os.getObject(objectId);
										int healthRatio = 0;
										healthRatio |= is.read();
										e.setHealtDirectly(healthRatio, 255);

										int fourByteFlag = 0;
										fourByteFlag |= is.read();
										fourByteFlag |= is.read() << 8;
										fourByteFlag |= is.read() << 16;
										fourByteFlag |= is.read() << 24;
										if ((fourByteFlag & F_INTACT) == F_INTACT) {
											e.addState(INTACT);
										} else if ((fourByteFlag & F_DAMAGED) == F_DAMAGED) {
											e.addState(DAMAGED);
										} else if ((fourByteFlag & F_BROKEN) == F_BROKEN) {
											e.addState(BROKEN);
										} else if ((fourByteFlag & F_BLASTED) == F_BLASTED) {
											e.addState(BLASTED);
										} else if ((fourByteFlag & F_BURNED) == F_BURNED) {
											e.addState(BURNED);
										}

										if ((fourByteFlag & F_OPEN) == F_OPEN) {
											e.addState(OPEN);
										} else if ((fourByteFlag & F_CLOSED) == F_CLOSED) {
											e.addState(CLOSED);
										} else if ((fourByteFlag & F_LOCKED) == F_LOCKED) {
											e.addState(LOCKED);
										}

										if ((fourByteFlag & F_BURNING) == F_BURNING) {
											e.addState(BURNING);
										}
										if ((fourByteFlag & F_DUSTY) == F_DUSTY) {
											e.addState(DUSTY);
										}
										if ((fourByteFlag & F_WET) == F_WET) {
											e.addState(WET);
										}
										if ((fourByteFlag & F_FROZEN) == F_FROZEN) {
											e.addState(FROZEN);
										}
										if ((fourByteFlag & F_WRITTEN) == F_WRITTEN) {
											e.addState(WRITTEN);
										}
										if ((fourByteFlag & F_FLOATING) == F_FLOATING) {
											e.addState(FLOATING);
										}
										if ((fourByteFlag & F_HIDDEN) == F_HIDDEN) {
											e.addState(HIDDEN);
										}

										if ((fourByteFlag & 0x80000000) == 0x80000000) {
											e.lockMovement();
										}
										if ((fourByteFlag & 0x40000000) == 0x40000000) {
											e.setDamageable(true);
										}

										ol.setObjectAt(x, y, e);
									} else {
										ol.setObjectAt(x, y, null);
									}
								}
							}
						} else {
							System.out.println("version code not recognized");
							for (int x = 0; x < this.map.getWidth(); x++) {
								for (int y = 0; y < this.map.getHeight(); y++) {
									int objectId = 0;
									objectId |= is.read();
									objectId |= is.read() << 8;
									objectId |= is.read() << 16;
									objectId |= is.read() << 24;
									ObjectSet os = CachedResources.singleton().getObjectSet(objectId);
									//									System.out.println("ID: " + objectId + " OS:" + os);
									if (os != null) {
										ol.setObjectAt(x, y, os.getObject(objectId));
									} else {
										ol.setObjectAt(x, y, null);
									}
								}
							}
						}
						//						System.out.println((System.currentTimeMillis() - actual));
						//						System.out.println(ol.toString()); // #######################################################################################################################
						bais.close();
					}
				}
			} //else if ("tileproperties".equalsIgnoreCase(nodeName)) { // TODO DISCUSS ABOUT THIS
				//				for (Node tpn = child.getFirstChild(); tpn != null; tpn = tpn.getNextSibling()) {
				//					if ("tile".equalsIgnoreCase(tpn.getNodeName())) {
				//						int x = getAttribute(tpn, "x", -1);
				//						int y = getAttribute(tpn, "y", -1);
				//
				//						Properties tip = new Properties();
				//
				//						readProperties(tpn.getChildNodes(), tip);
				//						ol.setTileInstancePropertiesAt(x, y, tip);
				//					}
				//				}
				//			}
		}
		// This is done at the end, otherwise the offset is applied during
		// the loading of the tiles. NOT USED
		//		ml.setOffset(offsetX, offsetY);

		// Invisible layers are automatically locked, so it is important to
		// set the layer to potentially invisible _after_ the layer data is
		// loaded.
		// todo: Shouldn't this be just a user interface feature, rather than
		// todo: something to keep in mind at this level?
		ol.setVisible(visible == 1);

		return ol;

		//		ObjectsLayer og = null;
		//		try {
		//			//				og = (ObjectsLayer) unmarshalClass(ObjectLayer.class, t);
		//		} catch (Exception e) {
		//			e.printStackTrace();
		//			return og;// og;
		//		}
		//
		//		//		final int offsetX = getAttribute(t, "x", 0); NOT USED?
		//		//		final int offsetY = getAttribute(t, "y", 0);
		//		//			og.setOffset(offsetX, offsetY);
		//
		//		// Add all objects from the objects group
		//		NodeList children = t.getChildNodes();
		//
		//		for (int i = 0; i < children.getLength(); i++) {
		//			Node child = children.item(i);
		//			if ("object".equalsIgnoreCase(child.getNodeName())) {
		//				//					og.addObject(readMapObject(child));
		//			}
		//		}
		//
		//		Properties props = new Properties();
		//		readProperties(children, props);
		//		//			og.setProperties(props);
		//
		//		return new ObjectsLayer(3, 3);// og;
	}

	/**
	 * Loads a map layer from a layer node.
	 * 
	 * @param t
	 *            the node representing the "layer" element
	 * @return the loaded map layer
	 * @throws IOException
	 */
	private Layer readLayer(Node t) throws IOException {
		final int layerWidth = getAttribute(t, "width", this.map.getWidth());
		final int layerHeight = getAttribute(t, "height", this.map.getHeight());

		MapLayer ml = new MapLayer(0, 0, layerWidth, layerHeight); // TODO START POINT

		//		final int offsetX = getAttribute(t, "x", 0);
		//		final int offsetY = getAttribute(t, "y", 0);
		final int visible = getAttribute(t, "visible", 1);
		String opacity = getAttributeValue(t, "opacity");

		ml.setName(getAttributeValue(t, "name"));

		if (opacity != null) {
			ml.setOpacity(Float.parseFloat(opacity));
		}

		readProperties(t.getChildNodes(), ml.getProperties());

		for (Node child = t.getFirstChild(); child != null; child = child.getNextSibling()) {
			String nodeName = child.getNodeName();
			if ("data".equalsIgnoreCase(nodeName)) {
				String encoding = getAttributeValue(child, "encoding");

				if ((encoding != null) && "base64".equalsIgnoreCase(encoding)) {
					Node cdata = child.getFirstChild();
					if (cdata != null) {
						char[] enc = cdata.getNodeValue().trim().toCharArray();
						byte[] dec = Base64.decode(enc);
						ByteArrayInputStream bais = new ByteArrayInputStream(dec);
						InputStream is;

						String comp = getAttributeValue(child, "compression");

						if ("gzip".equalsIgnoreCase(comp)) {
							final int len = layerWidth * layerHeight * 4;
							is = new GZIPInputStream(bais, len);
						} else if ("zlib".equalsIgnoreCase(comp)) {
							is = new InflaterInputStream(bais);
						} else if ((comp != null) && !comp.isEmpty()) {
							throw new IOException("Unrecognized compression method \"" + comp + "\" for map layer " + ml.getName());
						} else {
							is = bais;
						}

						for (int y = 0; y < this.map.getHeight(); y++) {
							for (int x = 0; x < this.map.getWidth(); x++) {
								int tileId = 0;
								tileId |= is.read();
								tileId |= is.read() << 8;
								tileId |= is.read() << 16;
								tileId |= is.read() << 24;

								Entry<Integer, TileSet> ts = this.findTileSetForTileGID(tileId);
								if (ts != null) {
									ml.setTileAt(x, y, ts.getValue().getTile(tileId - ts.getKey()));
								} else {
									ml.setTileAt(x, y, null);
								}
							}
						}
						bais.close();
					}
				} else {
					int x = 0, y = 0;
					for (Node dataChild = child.getFirstChild(); dataChild != null; dataChild = dataChild.getNextSibling()) {
						if ("tile".equalsIgnoreCase(dataChild.getNodeName())) {
							int tileId = getAttribute(dataChild, "gid", -1);
							Entry<Integer, TileSet> ts = this.findTileSetForTileGID(tileId);
							if (ts != null) {
								ml.setTileAt(x, y, ts.getValue().getTile(tileId - ts.getKey()));
							} else {
								ml.setTileAt(x, y, null);
							}

							x++;
							if (x == this.map.getWidth()) {
								x = 0;
								y++;
							}
							if (y == this.map.getHeight()) {
								break;
							}
						}
					}
				}
			} else if ("tileproperties".equalsIgnoreCase(nodeName)) {
				for (Node tpn = child.getFirstChild(); tpn != null; tpn = tpn.getNextSibling()) {
					if ("tile".equalsIgnoreCase(tpn.getNodeName())) {
						int x = getAttribute(tpn, "x", -1);
						int y = getAttribute(tpn, "y", -1);

						Properties tip = new Properties();

						readProperties(tpn.getChildNodes(), tip);
						ml.setTileInstancePropertiesAt(x, y, tip);
					}
				}
			}
		}
		// This is done at the end, otherwise the offset is applied during
		// the loading of the tiles. NOT USED
		//		ml.setOffset(offsetX, offsetY);

		// Invisible layers are automatically locked, so it is important to
		// set the layer to potentially invisible _after_ the layer data is
		// loaded.
		// todo: Shouldn't this be just a user interface feature, rather than
		// todo: something to keep in mind at this level?
		ml.setVisible(visible == 1);

		return ml;
	}

	private void buildMap(Document doc) throws MalformedURLException, IOException, ParserConfigurationException {
		Node item, mapNode;

		mapNode = doc.getDocumentElement();

		if (!"map".equals(mapNode.getNodeName())) {
			throw new RuntimeException("Not a valid tmx map file.");
		}

		// Get the map dimensions and create the map
		int mapWidth = getAttribute(mapNode, "width", 0);
		int mapHeight = getAttribute(mapNode, "height", 0);

		if ((mapWidth > 0) && (mapHeight > 0)) {
			this.map = new Map(mapWidth, mapHeight, this.player);
		} else {
			// Maybe this map is still using the dimensions element
			NodeList l = doc.getElementsByTagName("dimensions");
			for (int i = 0; (item = l.item(i)) != null; i++) {
				if (item.getParentNode() == mapNode) {
					mapWidth = getAttribute(item, "width", 0);
					mapHeight = getAttribute(item, "height", 0);

					if ((mapWidth > 0) && (mapHeight > 0)) {
						this.map = new Map(mapWidth, mapHeight, this.player);
					}
				}
			}
		}

		if (this.map == null) {
			throw new RuntimeException("Couldn't locate map dimensions.");
		}

		// Load other map attributes
		String orientation = getAttributeValue(mapNode, "orientation");
		if (orientation != null) {
			this.setOrientation(orientation);
		} else {
			this.setOrientation("orthogonal");
		}

		int tileWidth = getAttribute(mapNode, "tilewidth", 0);
		int tileHeight = getAttribute(mapNode, "tileheight", 0);

		if (tileWidth > 0) {
			this.map.setTileWidth(tileWidth);
		}
		if (tileHeight > 0) {
			this.map.setTileHeight(tileHeight);
		}

		int backgroundColor = getAttribute(mapNode, "backgroundcolor", 0);
		if (backgroundColor != 0) {
			this.map.setBackground(backgroundColor);
		}

		// Load properties
		readProperties(mapNode.getChildNodes(), this.map.getProperties());

		// Load tilesets first, in case order is munged
		this.tilesetPerFirstGid = new TreeMap<Integer, TileSet>();
		NodeList l = doc.getElementsByTagName("tileset");
		for (int i = 0; (item = l.item(i)) != null; i++) {
			this.map.addTileset(this.unmarshalTileset(item));
		}
		l = doc.getElementsByTagName("objectset");
		for (int i = 0; (item = l.item(i)) != null; i++) {
			this.map.addObjectset(this.unmarshalObjectset(item));
		}

		// Load the layers and objectgroups
		for (Node sibs = mapNode.getFirstChild(); sibs != null; sibs = sibs.getNextSibling()) {
			if ("layer".equalsIgnoreCase(sibs.getNodeName())) {//|| "objectgroup".equalsIgnoreCase(sibs.getNodeName())) {//TODO
				if (getAttributeValue(sibs, "name").equalsIgnoreCase("players")) {
					//					this.map.addLayer(null);
					continue;
				} else {
					Layer layer = this.readLayer(sibs);
					if (layer != null) {
						this.map.addLayer(layer);
					}
				}
			} else if ("objectgroup".equals(sibs.getNodeName())) {
				Layer layer = this.readObjectLayer(sibs);
				if (layer != null) {
					this.map.addLayer(layer);
				}
			}
		}
		this.tilesetPerFirstGid = null;
	}

	private Map unmarshal(InputStream in) throws ParserConfigurationException, IOException, ParseException {
		DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
		Document doc;
		try {
			factory.setIgnoringComments(true);
			factory.setIgnoringElementContentWhitespace(true);
			factory.setExpandEntityReferences(false);
			DocumentBuilder builder = factory.newDocumentBuilder();
			builder.setEntityResolver(this.entityResolver);
			InputSource insrc = new InputSource(in);
			insrc.setSystemId(this.xmlPath);
			insrc.setEncoding("UTF-8");
			doc = builder.parse(insrc);
		} catch (SAXException e) {
			e.printStackTrace();
			throw new ParseException("Error while parsing map file: " + e.toString(), 0);
		}

		this.buildMap(doc);

		return this.map;
	}

	/**
	 * Create a map, reading it from a valid TMX file
	 * 
	 * @param filename
	 *            the file where to read the map
	 * @param pl
	 *            the player entity
	 * @return The new <code>Map</code> created
	 * @throws MalformedURLException
	 * @throws IOException
	 * @throws ParseException
	 * @throws ParserConfigurationException
	 */
	public Map readMap(String filename, PlayerEntity pl) throws MalformedURLException, IOException, ParserConfigurationException, ParseException {
		this.player = pl;
		if (filename.startsWith("/")) {
			filename = filename.substring(1);
		}
		this.xmlPath = BASE_DIRECTORY + filename.substring(0, filename.lastIndexOf(File.separatorChar) + 1);

		String xmlFile = makeUrl(filename);
		//xmlPath = makeUrl(xmlPath);

		URL url = new URL(xmlFile);
		InputStream is = url.openStream();

		// Wrap with GZIP decoder for .tmx.gz files
		if (filename.endsWith(".gz")) {
			is = new GZIPInputStream(is);
		}

		Map unmarshalledMap = this.unmarshal(is);
		unmarshalledMap.setFilename(filename);

		this.map = null;

		return unmarshalledMap;
	}

	/**
	 * Create a <code>Map</code> object, using an <code>InputStream</code> input.
	 * 
	 * @param in
	 * @return the map
	 * @throws Exception
	 */
	public Map readMap(InputStream in) throws Exception {
		this.xmlPath = makeUrl(".");

		Map unmarshalledMap = this.unmarshal(in);

		//unmarshalledMap.setFilename(xmlFile)
		//
		return unmarshalledMap;
	}

	//	public TileSet readTileset(String filename) throws Exception {
	//		String xmlFile = filename;
	//
	//		xmlPath = filename.substring(0, filename.lastIndexOf(File.separatorChar) + 1);
	//
	//		xmlFile = makeUrl(xmlFile);
	//		xmlPath = makeUrl(xmlPath);
	//
	//		URL url = new URL(xmlFile);
	//		return unmarshalTilesetFile(url.openStream(), filename);
	//	}
	//
	//	public TileSet readTileset(InputStream in) throws Exception {
	//		return unmarshalTilesetFile(in, ".");
	//	}

	/**
	 * Checks if the file has a correct extension.
	 * 
	 * @param pathName
	 * @return true if acceptable
	 */
	public boolean accept(File pathName) {
		try {
			String path = pathName.getCanonicalPath();
			if (path.endsWith(".tmx") || path.endsWith(".tsx") || path.endsWith(".tmx.gz") || path.endsWith(".gmmx")) {
				return true;
			}
		} catch (IOException e) {
		}
		return false;
	}

	private class MapEntityResolver implements EntityResolver {
		@Override
		public InputSource resolveEntity(String publicId, String systemId) {
			if (systemId.equals("http://mapeditor.org/dtd/1.0/map.dtd")) {
				return new InputSource(TMXMapReader.class.getResourceAsStream("resources/map.dtd"));
			}
			return null;
		}
	}

	/**
	 * This utility function will check the specified string to see if it starts with one of the OS root designations.
	 * (Ex.: '/' on Unix, 'C:' on Windows)
	 * 
	 * @param filename
	 *            a filename to check for absolute or relative path
	 * @return <code>true</code> if the specified filename starts with a filesystem root, <code>false</code> otherwise.
	 */
	public static boolean checkRoot(String filename) {
		File[] roots = File.listRoots();

		for (File root : roots) {
			try {
				String canonicalRoot = root.getCanonicalPath().toLowerCase();
				if (filename.toLowerCase().startsWith(canonicalRoot)) {
					return true;
				}
			} catch (IOException e) {
				// Do we care?
			}
		}

		return false;
	}

	/**
	 * Get the tile set and its corresponding firstgid that matches the given global tile id.
	 * 
	 * 
	 * @param gid
	 *            a global tile id
	 * @return the tileset containing the tile with the given global tile id, or <code>null</code> when no such tileset
	 *         exists
	 */
	private Entry<Integer, TileSet> findTileSetForTileGID(int gid) {
		return this.tilesetPerFirstGid.floorEntry(gid);
	}

	//	private ObjectSet findObjectSetForObjectGID(int gid) {
	//		ArrayList<ObjectSet> os = CachedResources.singleton().getObjectSets();
	//		for (ObjectSet o : os) {
	//			if (o.contains(gid)) {
	//				return o;
	//			}
	//		}
	//		return null;
	//	}

	private void setFirstGidForTileset(TileSet tileset, int firstGid) {
		this.tilesetPerFirstGid.put(firstGid, tileset);
	}

	//	private void setFirstGidForObjectset(ObjectSet tileset, int firstGid) {
	//		CachedResources.get()getClass().
	//		this.objectsetPerFirstGid.put(firstGid, tileset);
	//	}
}
